define('framework/dataGrid/DataGridModule', [
    'angular',
    'jquery',
    'underscore',
    'config.properties',
    'artTmpl',
    'ngContextMenu',
    'framework/datasource/DataSourceModule',
    'framework/colSettings/ColSettingsModule',
    'framework/clickbox/GillionClickBoxModule',
    'framework/gridlayout/GridLayoutModule'
], function (angular, $, _, config, artTmpl) {
    var CONSTANTS = {
        CHECK_ROW_WIDTH: 40,
        SPACE_CELL_WIDTH: 19,
        MIN_CELL_WIDTH: 40,
        REFRESH_ROWS: '$grid-refresh-rows',
        DATA_COLUMN_INSTANCE_KEY: '$column-ins'
    }, defDisableCopyEditing = (function () {
        try {
            return config.controls.dataGrid.disableCopyEditing;
        } catch (e) {
        }
    } ());

    angular.module('DataGridModule', ['DataSourceModule', 'ColSettingsModule', 'GillionClickBoxModule', 'ng-context-menu', 'GridLayoutModule'])
        .directive('gDataGrid', function ($window, $document, $parse, $compile, $filter, $dataSourceManager, ColSettings, Arrays, Functions, gridLayoutService) {
            var GridProto = Grid.prototype,
                ORDER = {
                    ASC: 'asc',
                    DESC: 'desc'
                };

            function Grid(scope, element, attrs) {
                window.grid = this;
                var me = this;
                me.scope = scope;
                me.element = element;
                me.attrs = attrs;
                me.width = element.width();
                me.hash = scope.$id;
                me.widthStyleBlock = element.children('style')[0];

                me.cellValueGetterCache = {};
                me.scopeEventCache = {};
                scope.columns = me.columns = [];
                if (attrs.$columns) {
                    scope.$columns = me.columns;
                }
                me.footCellIndex = 0;
                /**
                 * @type {{String: [Number, Number]}}
                 */
                me.requiredClassNameColSpanRange = {};
                me.actions = {};

                me.tbody = element.find('table.table-body tbody')[0];
                me._disabledCheckboxRowIndexes = [];

                me.$headerBox = me.element.children('div.grid-head');
                me.$colResizeLine = me.element.find('div.col-resize-line');
                me.layoutSetting = {};
                me.$$modifiedRecords = {};
            }

            GridProto.autoWidthColumns = function () {
                var me = this;
                var $style = me.element.find('style')
                var $headerTh = me.$headerBox.find('th:visible');
                var $footTh = me.element.find('.table-foot').find('th:visible');
                var classList = [];
                var colSettings = me._getColSettings();
                $style.empty();
                $headerTh.removeAttr('width');
                var headerScopeList = [];
                $headerTh.each(function (index, el) {
                    var $el = $(el);
                    var headerScope = $el.isolateScope();
                    var classArr = $(el).attr('class').split(' ');
                    _.forEach(classArr, function (cls) {
                        if (cls.indexOf('grid-') === 0 && cls.indexOf('checkbox') < 0) {
                            classList.push(cls);
                            headerScopeList.push(headerScope);
                            return false;
                        } else if (cls.indexOf('checkbox') >= 0) {
                            var footClass = $footTh.first().attr('class').split(' ');
                            _.forEach(footClass, function (fcls) {
                                if (fcls.indexOf('grid-') === 0) {
                                    headerScopeList.push(headerScope);
                                    classList.push(fcls);
                                    return false;
                                }
                            })
                            return false;
                        }
                    });
                });
                var cssText = '';
                _.forEach(classList, function (cls, index) {
                    var $tds = me.element.find('.' + cls);
                    var maxWidth = 0;
                    if (!me.$headerBox.find('.' + cls).length) {
                        cssText += '.' + cls + '{width:' + me.$headerBox.find('.grid-col-checkbox').outerWidth() + 'px;}';
                        return;
                    }
                    $tds.each(function (index, el) {
                        var $el = $(el);
                        var width = $el.outerWidth();
                        if (width > maxWidth) {
                            maxWidth = width;
                        }
                    });
                    cssText += '.' + cls + '{width:' + maxWidth + 'px;}';
                    var col, oIdx, colSetting;
                    var headerScope = headerScopeList[index];
                    if (headerScope) {
                        col = headerScope.column;
                        oIdx = _.findIndex(me.__originalCols, col);
                        colSetting = _.findWhere(colSettings, { oIdx: oIdx });
                        colSetting.width = maxWidth;
                        col.width = colSetting.width;
                        me.layoutSetting = colSettings;
                    }
                });
                $style.html(cssText);
            }

            GridProto._registerHeaderDraggable = function () {
                var me = this;
                if (!me.scope.colSettingsKey) {
                    return;
                }
                var $headerBox = me.$headerBox,
                    $headerInput = $headerBox.find(':input'),
                    $colResizeLine = me.$colResizeLine,
                    preventInputFocus = function () {
                        $headerInput.each(function (i, el) {
                            el.blur();
                        })
                    }
                mousemoveFun = function ($event) {
                    var target = $event.target,
                        $th = angular.element(target).closest('th'),
                        width, offset, eX, inRightEdge, dragging;
                    var inDropFilter = !!($(target).closest('.drop-filter').length);
                    if ($th.length > 0) {
                        width = $th.outerWidth();
                        offset = $th.offset();
                        eX = $event.clientX;
                        inRightEdge = eX - offset.left > width - 5;
                        dragging = angular.isDefined($headerBox.data('_start_x'));
                        if (inRightEdge && !inDropFilter || dragging) {
                            $headerBox.addClass('resizeable');
                            if (me.scope.draggable) {
                                $th.attr("draggable", "false");
                            }
                            if (dragging) {
                                $headerInput.on('focus', preventInputFocus);
                                $colResizeLine.css('left', $event.clientX - me.element.offset().left + me.element.scrollLeft());
                            }
                        } else {
                            $headerBox.removeClass('resizeable');
                            if (me.scope.draggable) {
                                $th.attr("draggable", "true");
                            }
                        }
                    }
                };

                me.element.on('mouseup', function () {
                    $headerInput.off('focus', preventInputFocus);
                });
                $headerBox.mousemove(mousemoveFun);
                $headerBox.mousedown(function ($event) {
                    var target = $event.target,
                        $th = angular.element(target).closest('th');
                    if ($th.length > 0 && $headerBox.hasClass('resizeable')) {
                        me._startColResize($event, $th);
                        angular.element($document).one('mouseup', function ($event) {
                            me._finishColResize($event, $th);
                        });
                    }
                });

                /** 头部输入框的时候 可以拖动 begin **/
                if ($headerBox.find("input").size() > 0) {
                    var $input = $headerBox.find("input"),
                        $th = $input.parents("th");

                    $input.bind("focus", function () {
                        $headerBox.unbind("mousemove");
                        if (me.scope.draggable) {
                            $th.attr("draggable", "false");
                        }
                    });
                    $input.bind("blur", function ($event) {
                        if (me.scope.draggable) {
                            $th.attr("draggable", "false");
                            $headerBox.mousemove(mousemoveFun);
                        }
                    });
                }
                /** 头部输入框的时候 可以拖动  end **/
            };

            GridProto._startColResize = function ($event) {
                $('body').css({
                    webkitUserSelect: 'none',
                    mozUserSelect: 'none',
                    msUserSelect: 'none',
                    userSelect: 'none'
                });
                var me = this,
                    $headerBox = me.$headerBox,
                    $colResizeLine = me.$colResizeLine;
                $colResizeLine.css('left', $event.clientX - me.element.offset().left + me.element.scrollLeft());
                $colResizeLine.show();
                $headerBox.data('_start_x', $event.clientX);
                $window.document.onselectstart = function () {
                    return false;
                };
            };

            GridProto._finishColResize = function ($event, $th) {
                $('body').css({
                    webkitUserSelect: 'auto',
                    mozUserSelect: 'auto',
                    msUserSelect: 'auto',
                    userSelect: 'auto'
                });
                var me = this,
                    $colResizeLine = me.$colResizeLine,
                    $headerBox = me.$headerBox,
                    startX = $headerBox.data('_start_x'),
                    endX = $event.clientX,
                    added = endX - startX,
                    headerScope = $th.isolateScope();

                if (headerScope) {
                    var col = headerScope.column,
                        colSettings = me._getColSettings(),
                        oIdx = _.findIndex(me.__originalCols, col),
                        colSetting = _.findWhere(colSettings, { oIdx: oIdx }),
                        $styleBlock = me.element.children('style'),
                        colWidthClassName = col.colWidthClassName,
                        oldStyleContent = $styleBlock.html(),
                        newStyleContent;
                    newStyleContent = oldStyleContent.replace(getColClassReg(colWidthClassName), function (block, width) {
                        var newWidth = Number(width) + added;
                        newWidth = _.isNaN(newWidth) ? CONSTANTS.MIN_CELL_WIDTH : newWidth;
                        newWidth = Math.max(newWidth, CONSTANTS.MIN_CELL_WIDTH);
                        colSetting.width = newWidth;
                        col.width = colSetting.width;
                        me.layoutSetting = colSettings;
                        return block.replace(width, newWidth);
                    });

                    $styleBlock.html(newStyleContent);
                    me._reSortBody();
                    me.element.scroll();
                }
                $colResizeLine.hide();
                $headerBox.removeData('_start_x');

                $window.document.onselectstart = angular.noop;
            };

            function getColClassReg(colWidthClassName) {
                var matched = colWidthClassName.match(/grid-(.+)-col-(\d+)/),
                    colIndex = matched[2];
                return new RegExp('(?:\\' + colWidthClassName + '|\\\.grid-' + matched[1] + '-col-[0-' + colIndex + ']-[' + (colIndex + 1) + '-9])\\\{width:([\\\d\\\.]+)px;}', 'g');
            }

            /**
             * @public
             * @param recordOrIndex {Object/Number} 记录或记录在表格中的 index
             */
            GridProto.saveRecord = function (recordOrIndex) {
                var me = this,
                    recordIndex;
                if (_.isObject(recordOrIndex)) {
                    recordIndex = _.chain(me.scope.source).indexOf(recordOrIndex).value();
                } else if (_.isNumber(recordOrIndex)) {
                    recordIndex = recordOrIndex;
                }
                if (_.isNumber(recordIndex)) {
                    me.renderRowTitle(recordIndex);
                    me._reRenderRow(recordIndex);
                }
            };

            /**
             * @see Grid#saveRecord
             * @private
             *  重新生成行 `title`,
             *  <b>请不要使用这个方法， 这个方法被修改为私有了。</b>
             * @param param {Number/HTMLTableRowElement/function(rowData:Object):Boolean} 行号或行DOM
             */
            GridProto.renderRowTitle = function (param) {
                var me = this,
                    columns = me.columns,
                    source = me.scope.source,
                    trs = me.tbody.rows,
                    row, $row, rowIndex, rowData;
                if (angular.isNumber(param)) {
                    rowIndex = param;
                } else if (angular.isFunction(param)) {
                    rowIndex = _.findIndex(source, param);
                } else if (($row = angular.element(param)).is('tr')) {
                    rowIndex = $row[0].rowIndex;
                }
                if (row && rowIndex) {
                    row = trs[param];
                    rowData = source[rowIndex];
                    _.each(row.cells, function (i, cell) {
                        var columnIndex = me._getColumnIndex(cell),
                            column = columns[columnIndex];
                        me._renderCellTitle(rowData, column, cell);
                    });
                }
            };

            /**
             * @param rowData 行数据
             * @param column 列对象
             * @param $cellDom {HTMLTableCellElement} 列`dom`
             * @private
             */
            GridProto._renderCellTitle = function (rowData, column, $cellDom) {
                var me = this;
                if (!column.disableTitle) {
                    var cellValue = me.getCellValue(rowData, column);
                    angular.element($cellDom).attr('title', cellValue);
                }
            };

            GridProto._addColumn = function (column) {
                this._renderColumnFilter(column);
                return this.columns.push(column);
            };

            //渲染查询列
            GridProto._renderColumnFilter = function (column) {
                var me = this,
                    scope = me.scope,
                    dataSource = scope.dataSource,
                    filter = column.filter,
                    field = column.field,
                    $eleCol = column.element,
                    filterTmpl = column.filterTmpl,
                    $filter,
                    i, item;

                if (!filterTmpl) {
                    if (!filter) return;
                    switch (filter) {
                        case 'text':
                            filterTmpl = '<input type="text" class="form-text">';
                            break;
                        case 'number-spinner':
                            filterTmpl = '<g-number-spinner></g-number-spinner>';
                            break
                    }
                }
                if (!filterTmpl) return;
                $filter = angular.element(filterTmpl);
                for (i = 0; i < $filter.length; i++) {
                    if ($filter[i].nodeType == 3) {
                        continue;
                    }
                    item = $filter.eq(0);
                    if (!item.attr('ng-model')) {
                        item.attr({
                            'ng-model': 'filter.' + column.field
                        });
                    }
                }

                $eleCol.closest('table.table-head').addClass('has-filters');
                $eleCol.append($filter);
                $compile($filter)(scope);
                $filter.wrap('<div class="column-filter"></div>');
            };

            GridProto._getCellValue = function (row, column) {
                var me = this,
                    cellValueGetterCache = me.cellValueGetterCache;
                if (!cellValueGetterCache[column.data]) {
                    cellValueGetterCache[column.data] = me.generateCellValueGetter(column);
                }
                return cellValueGetterCache[column.data](row);
            };

            GridProto._generateCellValueGetter = function (column) {
                if (column.data) {
                    return $parse(column.data);
                }
                return angular.noop;
            };

            GridProto._getSortName = function (sortingIcon) {
                var me = this,
                    columnIndex = me._getColumnIndex(sortingIcon.closest('th'));
                return me.columns[columnIndex].sortable;
            };

            GridProto._isSort = function (sortName, sortDirection) {
                var me = this,
                    sortNames = me.sortName || [],
                    sortDirections = me.sortDirection || [],
                    index = _.indexOf(sortNames, sortName);
                if (index < 0) return false;
                return sortDirections[index] == sortDirection;
            };

            GridProto._sortIndex = function (sortName) {
                var me = this,
                    sortNames = me.sortName || [],
                    index = _.indexOf(sortNames, sortName),
                    len = sortNames.length;
                if (index > -1 && len > 1) {
                    return index + 1;
                } else {
                    return '';
                }
            };

            /**
             * @param $event
             * @param $sortingIcon
             * @param multiSort 多列排序
             */
            GridProto._specifySort = function ($event, $sortingIcon, multiSort) {
                var me = this,
                    $header = $sortingIcon.closest('th'),
                    column = $header.isolateScope().column,
                    newSortName = column.field,
                    newSortDirection = $sortingIcon.hasClass('caret-up') ? ORDER.ASC : ORDER.DESC,
                    sortNames = me.sortName = me.sortName || [],
                    sortDirections = me.sortDirection = me.sortDirection || [],
                    index = _.indexOf(sortNames, newSortName),
                    oldSortDirection;
                if (multiSort) {
                    if (index > -1) {
                        sortDirections[index] = newSortDirection;
                    } else {
                        sortNames.push(newSortName);
                        sortDirections.push(newSortDirection);
                    }
                } else {
                    if (sortNames.length == 1 && index > -1) {
                        oldSortDirection = sortDirections[index];
                        if (oldSortDirection === newSortDirection) {
                            sortNames = [];
                            sortDirections = [];
                        } else {
                            sortNames = [newSortName];
                            sortDirections = [newSortDirection];
                        }
                    } else {
                        sortNames = [newSortName];
                        sortDirections = [newSortDirection];
                    }
                }
                me._doSort(sortNames, sortDirections);
            };

            //修改增加多列排序
            GridProto._toggleSort = function ($event, multiSort) {
                var me = this,
                    $target = angular.element($event.target),
                    $header = $target.closest('th'),
                    isolateScope = $header.isolateScope(),
                    sortNames = me.sortName = me.sortName || [],
                    sortDirections = me.sortDirection = me.sortDirection || [],
                    column, newSortName, oldSortName, oldSortDirection, newSortDirection, index;
                if (!(isolateScope && isolateScope.column && isolateScope.column.sortable)) return;

                column = isolateScope.column;
                newSortName = column.field;
                newSortDirection = ORDER.ASC;
                index = _.indexOf(sortNames, newSortName);

                if (multiSort) {
                    if (index > -1) {
                        oldSortDirection = sortDirections[index];
                        if (oldSortDirection === ORDER.ASC) {
                            sortDirections[index] = ORDER.DESC;
                        } else if (oldSortDirection === ORDER.DESC) {
                            sortNames.splice(index, 1);
                            sortDirections.splice(index, 1);
                        }
                    } else {
                        sortNames.push(newSortName);
                        sortDirections.push(newSortDirection);
                    }
                } else {
                    if (sortNames.length == 1 && index > -1) {
                        oldSortDirection = sortDirections[index];
                        if (oldSortDirection === ORDER.ASC) {
                            sortDirections[index] = ORDER.DESC;
                        } else if (oldSortDirection === ORDER.DESC) {
                            sortNames = [];
                            sortDirections = [];
                        }
                    } else {
                        sortNames = [newSortName];
                        sortDirections = [newSortDirection];
                    }
                }
                me._doSort(sortNames, sortDirections);
            };

            GridProto._doSort = function (sortName, sortDirection) {
                if (this.__rendered !== true) return;
                var me = this,
                    scope = me.scope,
                    onBeforeSortColumn = scope.onBeforeSortColumn,
                    onSortColumn = scope.onSortColumn,
                    dataSource = scope.dataSource,
                    sortEventParam = {
                        grid: this,
                        sortName: sortName,
                        sortDirection: sortDirection
                    };
                if (onBeforeSortColumn(sortEventParam) !== false) {
                    //为了触发数据源请求
                    dataSource.sortName = me.sortName = sortName;
                    dataSource.sortDirection = me.sortDirection = sortDirection;
                    var allowAutoLoad = dataSource.$scope.allowAutoLoad;
                    if (!(allowAutoLoad === 'true' || allowAutoLoad === true)) {
                        dataSource.doRequestData();
                        me.$$modifiedRecords = {};
                    }
                    onSortColumn(sortEventParam);
                }
            };

            GridProto._desortIfSameAsOld = function (newSortName, newDirection) {
                var me = this,
                    oldSortName = me.sortName,
                    oldSortDirection = me.sortDirection;
                if (oldSortName === newSortName && oldSortDirection === newDirection) {
                    return undefined;
                }
                return newDirection;
            };

            GridProto.isAllRowsChecked = function () {
                return this.scope.allChecked;
            };

            GridProto._isAllRowsChecked = function () {
                var me = this,
                    scope = me.scope,
                    disabledRows = me._getDisabledRows(),
                    disabledCount = disabledRows.length,
                    sourceLen = scope.source.length,
                    enableCount = sourceLen - disabledCount,
                    checkedEnableCount = _.difference(scope.checkedRows, disabledRows).length;
                return checkedEnableCount === enableCount;
            };

            GridProto._getDisabledRows = function () {
                return _.chain(this.scope.source)
                    .map(_disabledLocalsTrans)
                    .filter(this._disabledRowPredicate())
                    .map(_.property("row"))
                    .value();
            };

            function _disabledLocalsTrans(row, index) {
                return {
                    row: row,
                    rowIndex: index
                };
            }

            GridProto._disabledRowPredicate = function () {
                var me = this,
                    express = me.checkboxesDisabledExpress;
                if (express) {
                    if (/\([$\w]+(?:,\s*[$\w]+)*\)$/.test(express)) {
                        return me._getScopeEvent(express);
                    } else {
                        return $parse(express);
                    }
                }
                return _.constant(false);
            };

            GridProto._toggleCellChecked = function ($event) {
                var $target = angular.element($event.target),
                    $checkbox = $target.closest('.form-clickbox'),
                    me, scope, source, checkedRows, rowIndex, row, checked;
                if ($checkbox.is('[disabled=disabled]')) return;
                me = this;
                scope = me.scope;
                source = scope.source;
                checkedRows = scope.checkedRows;
                rowIndex = $checkbox.closest('tr')[0].rowIndex;
                row = source[rowIndex];
                checked = _.contains(checkedRows, row);

                $checkbox.toggleClass('selected', !checked);
                if (checked) {
                    Arrays.remove(checkedRows, row);
                } else {
                    me.scope.checkedRows.push(source[rowIndex]);
                }
                // binded
                me.scope.allChecked = me._isAllRowsChecked();
                me.checkRowByContrl = true;
            };

            GridProto._toggleAllChecked = function () {
                var me = this,
                    trs = me.tbody.rows,
                    scope = me.scope,
                    source = scope.source,
                    checkedRows = scope.checkedRows,
                    disabledRows = me._getDisabledRows(),
                    enabledRows = Arrays.subtract(source, disabledRows),
                    i, len, record;
                checkedRows.length = 0;
                if (scope.allChecked) {
                    Arrays.pushAll(enabledRows, checkedRows);
                }
                for (i = 0, len = source.length; i < len; i++) {
                    record = source[i];
                    if (_.contains(enabledRows, record)) {
                        angular.element('td:first-child > div.form-clickbox', trs[i]).toggleClass('selected', scope.allChecked);
                    }
                }
                me.checkRowByContrl = true;
            };

            GridProto._isChecked = function (record) {
                var me = this,
                    scope = me.scope;
                if (scope.allChecked) {
                    return true;
                }
                if (_(scope.source).contains(record)) {
                    return _(scope.checkedRows).contains(record);
                } else {
                    var rowIdx = _(me.$$modifiedRecords).findKey(function (val) {
                        return val === record;
                    }),
                        originRecord = scope.source[rowIdx];
                    return _.contains(scope.checkedRows, originRecord);
                }
            };

            GridProto._getEnableCheckboxes$ = function () {
                return angular.element('td:first-child > div[disabled!=disabled]', this.tbody);
            };

            GridProto._syncCheckRowsByOuter = function () {
                var me = this,
                    scope = me.scope,
                    checkedRows = scope.checkedRows,
                    source = scope.source;
                if (angular.isArray(source) && source.length > 0 && angular.isArray(checkedRows)) {
                    angular.forEach(me.tbody.rows, function (tr) {
                        var rowIndex = tr.rowIndex,
                            rowData = source[rowIndex],
                            $checkbox = angular.element(tr).find('td.grid-col-checkbox > div.form-clickbox');
                        if (_.contains(checkedRows, rowData)) {
                            $checkbox.addClass('selected');
                        } else {
                            $checkbox.removeClass('selected');
                        }
                    });
                    me.scope.allChecked = me._isAllRowsChecked();
                }
            };

            GridProto._headClick = function ($event) {
                var me = this,
                    target = $event.target,
                    $target = angular.element(target),
                    $headTab = me.element.children('div.grid-head'),
                    $th = $target.closest('th');
                    multiSort = $event.ctrlKey,
                    $columnFilter = $target.closest('.column-filter').not($target),
                    $dropFilter = $target.closest('.drop-filter');
                if ($target.is('.drop-filter-toggle')) {
                    var $filterList = $target.siblings('.drop-filter-list');
                    $('.drop-filter-list').not($filterList).hide();
                    $filterList.toggle();
                }
                if ($dropFilter.length) {
                    $event.stopPropagation();
                    return;
                }
                if ($columnFilter.length) {
                    return;
                }
                if ($target.is('span.caret')) {
                    me._specifySort($event, $target, multiSort);
                } else if (!$headTab.hasClass('resizeable')) {
                    me._toggleSort($event, multiSort);
                }
                if ($target.is('a.fi') && $target.parent().is('.form-clickbox')) {
                    me._toggleAllChecked();
                }
            };

            GridProto._rowClick = function ($event, $row) {
                var me = this,
                    scope = me.scope,
                    rowIndex = $row[0].rowIndex,
                    oldSelected = scope.selectedRow,
                    selectedRecord = scope.source[rowIndex],
                    isSelected = !(scope.canToggleSelected === 'true' && oldSelected === selectedRecord),
                    onBeforeSelect = scope.onBeforeSelect,
                    onSelect = scope.onSelect,
                    selectParam = {
                        $event: $event,
                        record: selectedRecord,
                        grid: me,
                        $row: $row,
                        isSelected: isSelected
                    };

                //拖选功能start
                if (me.scope.dragSelected && me.scope.dragSelected.length > 0) {
                    for (var i = 0; i < me.scope.dragSelected.length; i++) {
                        var index = me.scope.dragSelected[i];
                        angular.element(me.tbody.rows[index]).removeAttr("active");
                    }
                }
                //end拖选功能

                if (onBeforeSelect(selectParam) !== false) {
                    scope.selectedRow = isSelected ? selectedRecord : undefined;
                    if (me.__oldSelectedRow) {
                        me.__oldSelectedRow.removeAttr('active')
                    }
                    if (isSelected) {
                        $row.attr('active', true);
                    }
                    me.__oldSelectedRow = $row;
                    onSelect(selectParam);
                }

            };

            GridProto._rowDbClick = function ($event, $row) {
                var me = this,
                    scope = me.scope,
                    selectedRecord = scope.selectedRow,
                    onBeforeRowDbclick = scope.onBeforeRowDbclick,
                    onRowDbclick = scope.onRowDbclick,
                    eventParam = {
                        $event: $event,
                        record: selectedRecord,
                        grid: me,
                        $row: $row
                    };

                if (onBeforeRowDbclick(eventParam) !== false) {
                    onRowDbclick(eventParam);
                }
            };

            GridProto._cellDbClick = function ($event, $cell) {
                var me = this,
                    scope = me.scope,
                    selectedRecord = scope.selectedRow,
                    columnIndex = me._getColumnIndex($cell),
                    column = me.columns[columnIndex],
                    globalBeforeCellDbclick = scope.onBeforeCellDbclick,
                    globalCellDbclick = scope.onCellDbclick,
                    onBeforeCellDbClick = angular.isFunction(column.onBeforeCellDbclick) ? column.onBeforeCellDbclick : angular.noop,
                    onCellDbClick = angular.isFunction(column.onCellDbclick) ? column.onCellDbclick : angular.noop,
                    eventParam = {
                        $event: $event,
                        record: selectedRecord,
                        grid: me,
                        $cell: $cell,
                        column: column
                    };

                if (globalBeforeCellDbclick(eventParam) !== false) {
                    globalCellDbclick(eventParam);
                }
                if (onBeforeCellDbClick(eventParam) !== false) {
                    onCellDbClick(eventParam);
                }
            };

            /**
             * 子指令scope定义方法属性时， 指定到grid外scope的某个方法， 通过本方法获取
             * @param eventDefineAttrVal 方法属性指定的值
             * @return {*}
             */
            GridProto._getScopeEvent = function (eventDefineAttrVal) {
                var me = this,
                    scopeEventCache = me.scopeEventCache,
                    gridOuterScope = me.scope.$parent;
                if (!!eventDefineAttrVal) {
                    if (!scopeEventCache[eventDefineAttrVal]) {
                        var parentGet = $parse(eventDefineAttrVal);
                        return function (locals) {
                            return parentGet(gridOuterScope, locals);
                        };
                    }
                    return scopeEventCache[eventDefineAttrVal];
                }
                return angular.noop;
            };

            GridProto._renderCellsStyle = function () {
                var me = this,
                    element = me.element,
                    allColWidths = me._getAllColWidths(),
                    colWidthClassBlocks = me._getColWidthClassBlocks(allColWidths),
                    requiredSpanColClassBlocks = me._getRequiredSpanColClassBlocks(allColWidths),
                    allColWidthClassBlocks = colWidthClassBlocks.concat(requiredSpanColClassBlocks),
                    cellWidthStyleBlockContent = _.reduce(allColWidthClassBlocks, Functions.sum),
                    $newCellStyleBlock = angular.element('<style>' + cellWidthStyleBlockContent + '</style>');
                element.children('style').remove();
                element.append($newCellStyleBlock);
                me._reSortBody();
            };

            GridProto._getVisibleColWidths = function () {
                var me = this,
                    element = me.element,
                    visiableColWidths = [],
                    $th = element.find(".grid-head").find("th");

                angular.forEach($th, function (th) {
                    var width,
                        display = angular.element(th).css("display");
                    if (display !== "none") {
                        width = angular.element(th).outerWidth();
                        visiableColWidths.push(width)
                    }
                })
                return visiableColWidths;
            };

            /**
             * 获取列的宽度样式片段
             *
             * @param allColWidths {Array<Number>} 所有列的宽度
             * @return {Array<String>} 宽度样式片段
             */
            GridProto._getColWidthClassBlocks = function (allColWidths) {
                var me = this,
                    colWidthClassBlocks = [],
                    colWidthClassName, colWidthClassBlock;
                angular.forEach(allColWidths, function (colWidth, colIndex) {
                    colWidthClassName = me._getColWidthClassName(colIndex);
                    colWidthClassBlock = _toWidthClassBlock(colWidthClassName, colWidth);
                    colWidthClassBlocks.push(colWidthClassBlock);
                });
                return colWidthClassBlocks;
            };

            /**
             * 获取跨列列的宽度样式片段
             *
             * @param allColWidths {Array<Number>} 所有列的宽度
             * @return {Array<String>} 宽度样式片段
             */
            GridProto._getRequiredSpanColClassBlocks = function (allColWidths) {
                var me = this,
                    spanColClassBlocks = [],
                    requiredClassNameColSpanRange = me.requiredClassNameColSpanRange,
                    start, end, spanColWidths, spanColWidthSum, spanColWidthClassBlock;
                angular.forEach(requiredClassNameColSpanRange, function (spanRange, requiredClassName) {
                    start = spanRange[0];
                    end = spanRange[1];
                    spanColWidths = allColWidths.slice(start, end);
                    spanColWidthSum = Arrays.reduce(spanColWidths, Functions.sum);
                    spanColWidthClassBlock = _toWidthClassBlock(requiredClassName, spanColWidthSum);
                    spanColClassBlocks.push(spanColWidthClassBlock);
                });
                return spanColClassBlocks;
            };

            function _toWidthClassBlock(className, width) {
                return '.' + className + '{width:' + width + 'px;}'
            }

            /**
             * 将 Array 中未定义的位置用余数的平均值填充, 如果平均值比最小值小， 用最小值填充
             * @example paddingWidthSpentAvg([1, undefined, 3, 4, undefined], 20, 2) -> [1, 6, 3, 4, 6]
             * @example paddingWidthSpentAvg([1, undefined, 3, 4, undefined], 20, 7) -> [1, 7, 3, 4, 7]
             * @param array {Array<Number>} 需要填充的数组
             * @param total {Number} 总值
             * @param min {Number} 最小值
             */
            function paddingWithSpentAvg(array, total, min) {
                var definedEls = Arrays.filter(array, angular.isDefined),
                    definedSum = Arrays.isNotEmptyArray(definedEls) ? Arrays.reduce(definedEls, Functions.sum) : 0,
                    undefinedCount = array.length - definedEls.length,
                    spent = total - definedSum,
                    spentAvg = spent / undefinedCount,
                    padding = Math.max(spentAvg, min);
                angular.forEach(array, function (el, index) {
                    if (angular.isUndefined(el)) {
                        array[index] = padding;
                    }
                });
            }

            /**
             * 获取所有列的宽度， 包括勾选列和补白列
             *
             * @return {Array<Number>} 所有列的宽度
             */
            GridProto._getAllColWidths = function () {
                var me = this,
                    __originalCols = me.__originalCols || me.columns,
                    gridContentWidth = me._getContentWidth(),
                    columnWidths;
                var colSettings = me._getColSettings();
                columnWidths = Arrays.transform(__originalCols, function (col) {
                    var oIdx = _.findIndex(me.__originalCols, col), colSetting;
                    if (oIdx >= 0 && _.isArray(colSettings)) {
                        colSetting = _.findWhere(colSettings, { oIdx: oIdx });
                        if (colSetting && colSetting.width) {
                            return colSetting.width;
                        }
                    }
                    return me._getSingleColWidth(col.widthDef, gridContentWidth);
                });
                paddingWithSpentAvg(columnWidths, gridContentWidth, CONSTANTS.MIN_CELL_WIDTH);
                me._setColWidth(columnWidths);
                if (me.hasCheckbox) {
                    var checkRowWidth = me.element.find('.table-head').find('.grid-col-checkbox').outerWidth();
                    columnWidths.unshift(checkRowWidth);
                }
                if (me._hasVerticalScroll()) {
                    columnWidths.push(CONSTANTS.SPACE_CELL_WIDTH);
                }
                return columnWidths;
            };
            //把当前表格每一列的宽度设置到columns中去
            GridProto._setColWidth = function (columnWidths) {
                var me = this,
                    len = me.__originalCols.length,
                    i, col;
                for (i = 0; i < len; i++) {
                    col = me.__originalCols[i];
                    col.width = "" + Number(columnWidths[i]).toFixed(1);
                }
            };
            GridProto._hasVerticalScroll = function () {
                var me = this,
                    $gridBody = me.element.find('.grid-body'),
                    $bodyTable = $gridBody.children('.table-body');
                return $gridBody.width() < $bodyTable.width();
            };

            /**
             * 获取表格单列宽度
             * @param columnWidthDef {String} 列指令的宽度定义字符串 例: 100 、100px 、 10%
             * @param gridContentWidth {Number} 表格主体宽度, 即数据展示部分的宽度
             * @return {number} 列宽度
             */
            GridProto._getSingleColWidth = function (columnWidthDef, gridContentWidth) {
                var widthPercent;
                if (!!columnWidthDef) {
                    if (/^[\d\.]+%$/.test(columnWidthDef)) {
                        widthPercent = Number(columnWidthDef.substring(0, columnWidthDef.length - 1));
                        return gridContentWidth * widthPercent / 100;
                    } else if (/^[\d\.]+px$/.test(columnWidthDef)) {
                        return Number(columnWidthDef.substring(0, columnWidthDef.length - 2));
                    } else if (/^[\d\.]+$/.test(columnWidthDef)) {
                        return Number(columnWidthDef);
                    }
                }
            };

            /**
             * 获取表格主体部分宽度，即数据展示部分的总宽度， 为表格外层div宽度减去勾选列和空白列的宽度
             * @private
             */
            GridProto._getContentWidth = function () {
                var me = this,
                    gridWidth = me.element.width(),
                    gridContentWidth = gridWidth - 2;
                if (me.hasCheckbox) {
                    gridContentWidth -= CONSTANTS.CHECK_ROW_WIDTH;
                }
                if (me._hasVerticalScroll()) {
                    gridContentWidth -= CONSTANTS.SPACE_CELL_WIDTH
                }
                return gridContentWidth;
            };

            GridProto._registerFootCellWidthClass = function (span) {
                var me = this,
                    startColIndex = me.footCellIndex,
                    colspan = isNaN(span) ? 1 : span,
                    endColIndex = startColIndex + colspan,
                    colWidthClassName = me._getColWidthClassName(startColIndex);
                if (colspan > 1) {
                    colWidthClassName += ('-' + endColIndex);
                    me.requiredClassNameColSpanRange[colWidthClassName] = [startColIndex, endColIndex];
                }
                me.footCellIndex += colspan;
                return colWidthClassName;
            };

            /**
             * @private
             * @param startColIndex {Number} 开始的`column`的`index`
             * @return {string}
             */
            GridProto._getColWidthClassName = function (startColIndex) {
                return 'grid-' + this.hash + '-col-' + startColIndex;
            };

            GridProto.getEditingRowIndex = function () {
                var me = this;
                return angular.isNumber(me.__editingRowIndex)
                    ? me.__editingRowIndex
                    : -1;
            }

            GridProto.startEdit = function (rowIndex) {
                var me = this,
                    scope = me.scope,
                    outerScope = scope.$parent,
                    columns = me.columns,
                    rowScope, tr, originRecord, originNgModel;
                if (!angular.isNumber(me.__editingRowIndex)) {
                    originRecord = me.scope.source[rowIndex];
                    tr = me.tbody.rows[rowIndex];
                    rowScope = getNewerEditingScope();
                    angular.forEach(tr.cells, function (cell) {
                        var $cell = angular.element(cell),
                            columnIndex = me._getColumnIndex($cell),
                            column = columns[columnIndex],
                            $editorPlace = $cell.children('[data-role=editor]'),
                            $displayPlace = $cell.children('[data-role=display]'),
                            columnEditorTmpl = me._getEditorTmpl(column),
                            $editor;

                        if (columnEditorTmpl) {
                            $editor = angular.element(columnEditorTmpl);
                            originNgModel = $editor.attr('ng-model');
                            if (originNgModel) {
                                if (!/^editingRecord\./.test(originNgModel)) {
                                    if (/^row\./.test(originNgModel)) {
                                        $editor.attr('ng-model', originNgModel.replace(/^row\./, "editingRecord."));
                                    } else {
                                        $editor.attr('ng-model', ("editingRecord." + originNgModel));
                                    }
                                }
                            } else {
                                $editor.attr('ng-model', ('editingRecord.' + column.field));
                            }
                            if (!$editor.attr('name')) {
                                $editor.attr('name', column.field);
                            }
                            if (me.scope.validatorName) {
                                $editor.attr('g-field-validator', '');
                                $editor.data('$formController', me.formController);
                                $editor.data('$gValidatorController', me.gValidatorController);
                                $editor.data('$dataSource', me.scope.source);
                            }
                            $displayPlace.hide();
                            $editorPlace.append($editor);
                        }
                    });
                    $compile(tr)(rowScope);
                    me.__editingRowIndex = rowIndex;

                    function getNewerEditingScope() {
                        var copy = me.$$modifiedRecords[rowIndex];
                        if (!copy) {
                            copy = me.$$modifiedRecords[rowIndex] = {};
                            angular.copy(originRecord, copy);
                        }

                        var newerScope = outerScope.$new(false);
                        newerScope.row = originRecord;
                        newerScope.record = originRecord;
                        newerScope.rowIndex = rowIndex;
                        newerScope.grid = me;
                        newerScope.source = scope.source;

                        if (me._isCopyEditingDisabled()) {
                            newerScope.editingRecord = originRecord;
                        } else {
                            newerScope.editingRecord = copy;
                        }
                        return newerScope;
                    }
                }
            };

            // force: boolean 强制结束编辑，跳过校验
            GridProto.finishEdit = function (force) {
                var me = this,
                    formController = me.formController;
                if (angular.isNumber(me.__editingRowIndex)) {
                    if (!force && formController) {
                        formController.verify();
                        if (!formController.$valid) return;
                    }
                    /** 联想控件特殊处理 begin**/
                    var trs = me.tbody.rows;
                    var row = trs[me.__editingRowIndex];
                    angular.element(row)
                        .find('.form-suggestbox')
                        .each(function (index, td) {
                            angular.element(td).scope().$destroy();
                        });
                    /** 联想控件特殊处理 end**/
                    me._reRenderRow(me.__editingRowIndex);
                    delete me.__editingRowIndex;
                }
            };

            GridProto.getModifiedRows = function () {
                return angular.element(this.tbody).find('tr.-grid-row-modified');
            };

            GridProto.getModifiedRecords = function () {
                return _(this.getModifiedRecordMap()).values();
            };

            GridProto.getModifiedRecordMap = function () {
                var me = this,
                    source = me._isCopyEditingDisabled() ? me.scope.source : me.$$modifiedRecords;
                return _.chain(me.getModifiedRows())
                    .map(_.property('rowIndex'))
                    .map(function (rowIndex) {
                        return [rowIndex, source[rowIndex]];
                    })
                    .object()
                    .value();
            };

            GridProto.flushModified = function () {
                var me = this,
                    source = me.scope.source;
                me.getModifiedRows().each(function (i, tr) {
                    var rowIndex = tr.rowIndex;

                    if (!me._isCopyEditingDisabled()) {
                        source.splice(rowIndex, 1, me.$$modifiedRecords[rowIndex]);
                    }

                    delete me.$$modifiedRecords[rowIndex];
                    me._reRenderRow(rowIndex);
                });
            };

            function makeEmpty(obj) {
                for (var key in obj) {
                    if (obj.hasOwnProperty(key))
                        delete obj[key];
                }
            }

            /**
             * 导出表格内容到 EXCEL 文件
             * @param fileName {String} 文件名，不需要扩展名
             */
            GridProto.exportToFile = function (fileName) {
                var me = this;
                var $grid = me.element;
                var $trs = $grid.find('tr:visible')
                var $trsClone = $trs.clone();
                var $temp = $('<div style="height: 1px; position: absolute; position: fixed; left: 99999px;">');
                var content = '';
                var BOM = "\uFEFF";
                $temp.append($trsClone);
                $('body').append($temp);
                $trsClone.each(function (iTr, tr) {
                    var $tr = $(tr);
                    var $tds = $tr.find('th:visible, td:visible');
                    var tdLen = $tds.length;
                    $tds.each(function (iTd, td) {
                        var $td = $(td);
                        $td.find(':input, :button, :checkbox, [data-role="editor"], .drop-filter, .column-filter').remove();
                        var tdTxt = wrapQuotes(replaceAll($.trim($td.text()), '"', '""'));
                        content += tdTxt + '\t';
                        if (iTd < tdLen - 1) {
                            content += ','
                        }
                    })
                    content += '\r\n';
                });
                $temp.remove();
                downloadFile(fileName + '.csv', BOM + content);

                function wrapQuotes(str) {
                    return '"' + str + '"';
                }

                function replaceAll(str, oldChar, newChar) {
                    return str.split('oldChar').join(newChar);
                }

                function downloadFile(fileName, content) {
                    var aLink = document.createElement('a');
                    var blob = new Blob([content], { type: 'text/csv' });
                    var evt = document.createEvent("HTMLEvents");
                    evt.initEvent("click", false, false);
                    aLink.download = fileName;
                    aLink.href = URL.createObjectURL(blob);
                    aLink.dispatchEvent(evt);
                }
            }

            GridProto.reverseModified = function () {
                var me = this;
                if (me._isCopyEditingDisabled()) {
                    me.getModifiedRows().each(function (i, tr) {
                        var rowIndex = tr.rowIndex,
                            originRecord = me.scope.source[rowIndex];
                        makeEmpty(originRecord);
                        angular.copy(me.$$modifiedRecords[rowIndex], originRecord);
                        delete me.$$modifiedRecords[rowIndex];
                        me._reRenderRow(rowIndex);
                    });
                } else {
                    me.getModifiedRows().each(function (i, tr) {
                        var rowIndex = tr.rowIndex;
                        delete me.$$modifiedRecords[rowIndex];
                        me._reRenderRow(rowIndex);
                    });
                }
            };

            GridProto._isCopyEditingDisabled = function () {
                if (this.scope.disableCopyEditing) {
                    return this.scope.disableCopyEditing === 'true';
                }
                return defDisableCopyEditing;
            };

            GridProto._reRenderRow = function (rowIndex) {
                var me = this,
                    trs = me.tbody.rows,
                    oldTr = trs[rowIndex],
                    $oldTr = angular.element(oldTr),
                    originRecord = me.scope.source[rowIndex];

                if (!originRecord) return;

                if (me.$$modifiedRecords.hasOwnProperty(rowIndex)) {
                    var copy = me.$$modifiedRecords[rowIndex],
                        newRow, i, len, field, cellIndex, cell;
                    if (me._isCopyEditingDisabled()) {
                        newRow = replaceRow(originRecord);
                    } else {
                        newRow = replaceRow(copy);
                    }
                    for (i = 0, len = me.columns.length; i < len; i++) {
                        field = me.columns[i].field;
                        if (field && (String(copy[field]) !== String(originRecord[field]))) {
                            cellIndex = me.hasCheckbox ? i + 1 : i;
                            cell = newRow.cells[cellIndex];
                            angular.element(cell).addClass('-grid-cell-modified');
                        }
                    }
                    if (cellIndex && cellIndex > 0) { // cellIndex > 0 说明有列被更改过
                        angular.element(newRow).addClass('-grid-row-modified');
                    }
                } else {
                    replaceRow(originRecord);
                }

                function replaceRow(record) {
                    var newRow;
                    $oldTr.replaceWith(me._getCompiledRowTmpl()({
                        record: record,
                        rowIndex: rowIndex,
                        grid: me
                    }));
                    newRow = trs[rowIndex];
                    $compile(newRow)(me.scope);
                    return newRow;
                }

            };

            GridProto._getEditorTmpl = function (column) {
                var me = this,
                    columnTmplMap = {};
                GridProto._getEditorTmpl = function (column) {
                    if (column) {
                        var field = column.field,
                            columnTmpl;
                        if (field) {
                            if (column.canEdit) {
                                // if (!columnTmplMap.hasOwnProperty(field)) {
                                if (column.editorTmpl) {
                                    columnTmplMap[field] = column.editorTmpl;
                                } else if (column.editable) {
                                    switch (column.editable) {
                                        case 'text':
                                            columnTmplMap[field] = '<input type="text" class="form-text">';
                                            break;
                                        case 'number-spinner':
                                            columnTmplMap[field] = '<g-number-spinner></g-number-spinner>';
                                            break;
                                        case 'checkbox':
                                            columnTmplMap[field] = '<g-checkbox></g-checkbox>';
                                            break;
                                    }
                                }
                                // }
                                // 如果能编辑， 返回模板
                                columnTmpl = columnTmplMap[field];
                                if (!columnTmpl) {
                                    column.canEdit = false;
                                } else {
                                    return columnTmpl;
                                }
                            }
                        }
                    }
                };
                me._getEditorTmpl(column);
            };

            /**
             *
             * @param cellScope.rowIndex
             * @param cellScope.column
             * @param cellScope.row
             * @param cellScope.columnIndex
             * @returns {void|*}
             */
            GridProto._getCellTmplScope = function (cellScope) {
                var me = this,
                    scope = me.scope,
                    gridOuterScope = scope.$parent,
                    rowIndex = cellScope.rowIndex,
                    formName = me.formName,
                    cellTmplScope = angular.extend(gridOuterScope.$new(), {
                        grid: me,
                        column: cellScope.column,
                        rowIndex: rowIndex,
                        columnIndex: cellScope.columnIndex,
                        row: cellScope.row
                    });
                if (formName) {
                    cellTmplScope[formName] = scope[formName];
                }
                return cellTmplScope;
            };

            /**
             * 重置表格状态
             * 包括：   1、 全选状态
             *        2、 勾选行
             *        3、 选中行
             */
            GridProto._reset = function () {
                var me = this,
                    scope = me.scope;
                // 清空选中行
                scope.allChecked = false;
                scope.selectedRow = scope.$selectedRow = undefined;
                delete me.__editingRowIndex;
                if (_.isArray(scope.$checkedRows)) {
                    scope.$checkedRows.length = 0;
                }
            };

            GridProto._getRowTmpl = function () {
                var me = this, columnsTmpl;
                if (!me.rowTmpl) {
                    columnsTmpl = _
                        .chain(me.columns)
                        .filter(angular.isDefined)
                        .map(function (column) {
                            var hiddenClass = column.hidden ? 'hidden' : '';
                            if (me._isForbiddenColumn(column.field)) {
                                return '';
                            }
                            if (column.hide === true) return '';
                            var cellTmpl = '<td ';
                            if (column.gItemClass) {
                                cellTmpl += 'render-cell-class outer-scope="grid.scope.$parent" '
                            }
                            cellTmpl += 'class="' + column.colWidthClassName + ' ' +
                            column.cellAlignClass + ' ' + hiddenClass + '"';
                            if (column.tmpl) {
                                cellTmpl += 'cell-templated><span data-role="display" ></span>'
                            } else if (column.data) {
                                cellTmpl += 'title="{{record.' + column.data + '}}"><span data-role="display">{{record.' + column.data + '}}</span>'
                            } else {
                                cellTmpl += '>'
                            }
                            if (column.editable || column.editorTmpl) {
                                cellTmpl += '<span data-role="editor"></span>';
                            }
                            return cellTmpl + '</td>';
                        })
                        .join('');
                    me.rowTmpl = '<tr{{if grid.rowClassController}} render-row-class outer-scope="grid.scope.$parent"{{/if}} {{if enableEvenClass === \'true\' && rowIndex % 2 === 1}} class="table-body-tr-even"{{/if}}>{{if grid.hasCheckbox}}\n<td class="grid-col-checkbox"  ng-style="dragStyle">\n    <div class="form-clickbox{{if grid._isChecked(record)}} selected{{/if}}" render-checkbox-disabled\n         outer-scope="grid.scope.$parent"><a href="javascript:void(0);" class="fi"></a></div>\n</td>{{/if}}' + columnsTmpl + '</tr>'
                }
                return me.rowTmpl;
            };


            GridProto._getCompiledRowTmpl = function () {
                var me = this;
                if (!me.compiledRowTmpl) {
                    me.compiledRowTmpl = artTmpl.compile(me._getRowTmpl());
                }
                return me.compiledRowTmpl
            };

            GridProto._getCompiledTableTmpl = function () {
                var me = this;
                if (!me.compiledTableTmpl) {
                    me.compiledTableTmpl = artTmpl.compile('{{each source as record rowIndex}}' + me._getRowTmpl() + '{{/each}}');
                }
                return me.compiledTableTmpl;
            };

            GridProto._registerHelpers = function () {
                var me = this,
                    columns;
                if (me.doneRegisterHelpers !== true) {
                    columns = me.columns;
                    _.chain(columns)
                        .map(_.property('data'))
                        .filter(_.negate(_.isEmpty))
                        .map(function (data) {
                            var firstVLPos = data.indexOf('|'),
                                filterString;
                            if (firstVLPos !== -1) {
                                filterString = data.substring(firstVLPos);
                                return filterString.match(/\|\s*[\$_\w\d]+/g)
                            }
                        })
                        .filter(_.negate(_.isEmpty))
                        .flatten()
                        .map(function (x) {
                            return $.trim(x.substring(1));
                        })
                        .each(function (helperName) {
                            artTmpl.helper(helperName, function () {
                                return $filter(helperName).apply($window, arguments);
                            });
                        });
                    me.doneRegisterHelpers = true;
                }
            };

            GridProto._syncRows = function (newSource, oldSource) {
                if (_.isUndefined(newSource)) return;
                var me = this,
                    scope = me.scope,
                    tbody = me.tbody,
                    trs = tbody.rows,
                    $rows = me.element.find('.table-body tr'),
                    added = Arrays.subtract(newSource, oldSource),
                    removed = Arrays.subtract(oldSource, newSource),
                    balanceCount = added.length - removed.length,
                    minRowIndex = $rows.length,
                    rowIndex, row, $newRow;
                angular.forEach(removed, function (r) {
                    rowIndex = Arrays.indexOf(oldSource, r);
                    Arrays.remove(oldSource, r);
                    row = trs[rowIndex];
                    row.parentNode.removeChild(row);
                    minRowIndex = Math.min(minRowIndex, rowIndex);
                });
                angular.forEach(added, function (r) {
                    rowIndex = Arrays.indexOf(newSource, r);
                    $newRow = angular.element(tbody.insertRow(rowIndex));
                    $newRow.replaceWith(me._getCompiledRowTmpl()({
                        record: r,
                        rowIndex: rowIndex,
                        grid: me
                    }));
                    $compile(trs[rowIndex])(scope);
                    minRowIndex = Math.min(minRowIndex, rowIndex);
                });
                me._keepLazyViewLength(balanceCount);
                me._refreshRows(minRowIndex);
            };

            GridProto._keepLazyViewLength = function (balanceCount) {
                var me = this,
                    scope = me.scope,
                    $renderedRows, renderedCount;
                if (scope.lazy === 'true') {
                    if (balanceCount > 0) {
                        $renderedRows = me.element.find('.grid-body tr');
                        renderedCount = $renderedRows.length;
                        $renderedRows.slice(renderedCount - balanceCount, renderedCount).remove();
                    } else {
                        var nextPage = me._getLazyNextPage(),
                            limit = nextPage * me._getLazyPageSize(),
                            rowDatas = scope.source.slice(limit + balanceCount, limit);
                        me._appendRowsAndStates(rowDatas);
                    }
                }
            };

            /**
             * 将指定<b>位置</b>或<b>数据对象</b>的行移动到指定位置， 默认移动到第一行
             *
             * @param rowDataOrIndex [Object/Number] 行数据或在`source`中的位置
             * @param [index=0] 移动到第几行， 默认移动到第一行
             */
            GridProto.moveRowTo = function (rowDataOrIndex, index) {
                var me = this,
                    scope = me.scope,
                    source = scope.source,
                    tbody = me.tbody,
                    trs = tbody.rows,
                    fromIndex = angular.isNumber(rowDataOrIndex) ? rowDataOrIndex : _.indexOf(source, rowDataOrIndex),
                    toIndex = angular.isNumber(index) ? index : 0,
                    rowData, fromTr, toTr, tmp;
                if (fromIndex >= 0 && toIndex >= 0 && (fromIndex !== toIndex)) {
                    if (_.has(me.$$modifiedRecords, fromIndex)) {
                        me.$$modifiedRecords[toIndex] = me.$$modifiedRecords[fromIndex];
                        delete me.$$modifiedRecords[fromIndex];
                    }
                    if (_.has(me.$$modifiedRecords, toIndex)) {
                        me.$$modifiedRecords[toIndex + 1] = me.$$modifiedRecords[toIndex];
                        delete me.$$modifiedRecords[toIndex];
                    }

                    rowData = source[fromIndex];
                    source.splice(fromIndex, 1);
                    source.splice(toIndex, 0, rowData);

                    fromTr = trs[fromIndex];
                    toTr = trs[toIndex];
                    if (fromTr && toTr) {
                        tbody.insertBefore(trs[fromIndex], trs[toIndex]);
                        me._refreshRows(Math.min(fromIndex, toIndex), Math.max(fromIndex, toIndex) + 1);
                    } else if (scope.lazy === 'true' && (fromTr || toTr)) {
                        if (fromTr && !toTr) {
                            tbody.removeChild(fromTr);
                            me._keepLazyViewLength(-1);
                        } else {
                            tbody.insertRow(index);
                            $(trs[index]).replaceWith(me._getCompiledRowTmpl()({
                                rowIndex: fromIndex,
                                record: rowData,
                                grid: me,
                                enableEvenClass: scope.enableEvenClass
                            }));
                            $compile(trs[index])(scope);
                            me._keepLazyViewLength(1);
                        }
                        me._refreshRows(Math.min(fromIndex, toIndex), trs.length);
                    }
                }
            };

            GridProto._refreshRows = function (start, end) {
                var me = this,
                    scope = me.scope,
                    gridOuterScope = scope.$parent,
                    trs = Array.prototype.slice.apply(me.tbody.rows, arguments),
                    ending = end || scope.source.length,
                    range = _.range(start, ending);
                if (start >= 0 && start < ending) {
                    angular.forEach(trs, function (index, tr) {
                        angular.element(tr).toggleClass('table-body-tr-even', tr.rowIndex % 2 === 1)
                    });
                    gridOuterScope.$broadcast(CONSTANTS.REFRESH_ROWS, range);
                }
            };

            GridProto._getLazyPageSize = function () {
                var me = this;
                return _.result(me, '_lazyPageSize', function () {
                    var height = me.element.height();
                    return me._lazyPageSize = Math.ceil(height / 21);
                });
            };

            GridProto._getLazyNextPage = function () {
                if (_.isNumber(this._lazyPage)) {
                    return (this._lazyPage + 1);
                }
                return 0;
            };

            GridProto._getLazyPageData = function (page, pageSize) {
                var me = this,
                    source = me.scope.source,
                    start = page * pageSize,
                    end;
                if (start >= source.length) {
                    return [];
                } else {
                    end = Math.min(source.length, start + pageSize);
                    return source.slice(start, end);
                }
            };

            GridProto._renderNextLazyPage = function () {
                var me = this,
                    page = me._getLazyNextPage(),
                    pageSize = me._getLazyPageSize(),
                    pageData = me._getLazyPageData(page, pageSize),
                    hasNextPage = !_.isEmpty(pageData);
                if (hasNextPage) {
                    me._lazyPage = page;
                    me._appendRowsAndStates(pageData)
                }
                return [];
            };

            GridProto._appendRowsAndStates = function (rowDatas) {
                var me = this,
                    scope = me.scope,
                    outerScope = scope.$parent,
                    $tbody = me.element.find('.grid-body tbody'),
                    firstRowIndex = _.indexOf(me.scope.source, rowDatas[0]),
                    rowIndexRange = _.range(firstRowIndex, firstRowIndex + me._getLazyPageSize()),
                    rowsSelector = firstRowIndex === 0 ? 'tr' : 'tr:gt(' + (firstRowIndex - 2) + ')',
                    $rows;
                $tbody.append(_.chain(rowDatas)
                    .map(function (record, index) {
                        return {
                            rowIndex: rowIndexRange[index],
                            record: record,
                            grid: me,
                            enableEvenClass: scope.enableEvenClass
                        };
                    })
                    .map(me._getCompiledRowTmpl())
                    .value()
                    .join(''));
                $rows = $tbody.children(rowsSelector);
                $compile($rows)(me.scope);
                if (!outerScope.$$phase && !outerScope.$root.$$phase) {
                    outerScope.$digest();
                }
            };

            GridProto._getColumnIndex = function (cellIndexOrDom) {
                var cellIndex;
                if (angular.isNumber(cellIndexOrDom)) {
                    cellIndex = cellIndexOrDom;
                } else {
                    cellIndex = angular.element(cellIndexOrDom)[0].cellIndex;
                }
                return this.hasCheckbox ? (cellIndex - 1) : cellIndex;
            };

            GridProto._render = function () {
                var me = this,
                    scope = me.scope,
                    oldTbody = me.element.find('table.table-body tbody'),
                    tbody;

                oldTbody.replaceWith('<tbody></tbody>').data('$gDataGridController', me);
                tbody = me.tbody = me.element.find('table.table-body tbody')[0];

                if (scope.lazy === 'true') {
                    delete me._lazyPage;
                    me._renderNextLazyPage();
                    me._renderNextLazyPage();
                } else {
                    tbody.innerHTML = me._getCompiledTableTmpl()(scope);
                    $compile(tbody)(scope);
                }

                if (!_(me.$$modifiedRecords).isEmpty()) {
                    _(me.$$modifiedRecords).forEach(function (v, k) {
                        me._reRenderRow(k);
                    });
                }

                //重载数据里添加对拖选功能支持
                if (me.scope.dragCheck === "true") {
                    me.element.find('table.table-body tbody tr').on("mouseover", function ($event) {
                        var target = $event.target;
                        if (me.scope.isMouseDown === true && target.className === "grid-col-checkbox") {
                            var $target = angular.element($event.target), rowIndex,
                                $checkbox = $target.find('.form-clickbox');
                            rowIndex = $checkbox.closest('tr')[0].rowIndex;
                            me.scope.endIndex = rowIndex;
                            me._toggleDragChecked($event, me.scope.startIndex, me.scope.endIndex);
                        }
                    });
                }

                me._reset();
            };

            GridProto._restColSettingsBtn = function () {
                var me = this,
                    scope = me.scope,
                    $headRow, headers, firstVisibleHead;
                if (!!scope.colSettingsKey) {
                    $headRow = me.element.find('div.grid-head tr:last');
                    $headRow.find('.set-btns').remove();
                    headers = $headRow.children('th');
                    if (me.hasCheckbox) {
                        headers = _.rest(headers);
                    }
                    firstVisibleHead = _.find(headers, function (header) {
                        return header.style.display !== 'none';
                    });
                    angular.element(firstVisibleHead).append($compile('<div class="set-btns"><i data-col-settings class="iconfont2 fi-set set-btn" ng-click="grid._startSetting($event)"></i> <i data-col-settings class="fi fi-refresh-small set-btn" ng-click="grid._clearFilter($event)"></i></div>')(scope));
                }
            };

            GridProto._enterCols = function (cols) {
                var me = this,
                    scope = me.scope;
                me.finishEdit(true);
                me._storeColumns(cols);
                me._reSortHeaders();
                me._reSortBody();
                me._reSortFooters();
                me._restColSettingsBtn();
                me._restAllTmpl();
                me._registerHelpers();
                if (!(scope.$$phase || scope.$root.$$phase)) {
                    scope.$apply(function () {
                        me._render();
                    });
                } else {
                    me._render();
                }
            };

            GridProto._reSortBody = function () {
                var me = this,
                    totalWidth = 0,
                    allColWidths = me._getVisibleColWidths(),
                    gridBodyChild = angular.element(me.element.find(".grid-body").children()[0]);
                angular.forEach(allColWidths, function (width) {
                    totalWidth = totalWidth + Number(width);
                });
                gridBodyChild.width(totalWidth - 22);
                gridBodyChild.css("min-height", 1);
            }

            GridProto._loadAndReSortHeadersIfStored = function () {
                var me = this, colSettings, content;
                if (me.scope.colSettingsKey) {
                    if (_.isEmpty(me.layoutSetting)) {
                        content = _.map(me.__originalCols, function (col) {
                            var index;
                            index = _.indexOf(me.__originalCols, col);
                            return {
                                oIdx: index
                            }
                        });
                        me.layoutSetting = content;
                    }
                    colSettings = me.layoutSetting;
                }
                if (colSettings) {
                    //me.__originalCols = me.columns;
                    me.columns = _.map(colSettings, function (colSetting) {
                        return me.__originalCols[colSetting.oIdx];
                    });
                } else {
                    me.columns = me.__originalCols;
                }
                me._reSortHeaders();
                me._reSortFooters();
            };

            GridProto._getColSettings = function () {
                var me = this;
                return me.layoutSetting;
            };

            GridProto._storeColumns = function (cols) {
                var me = this;
                if (_.isUndefined(me.__originalCols)) {
                    me.__originalCols = me.columns;
                }
                me.columns = cols;

                me.layoutSetting = _.map(cols, function (col) {
                    var index, group, width;
                    index = _.indexOf(me.__originalCols, col);
                    width = col.width;
                    group = col.group || me.__originalCols[index].group;
                    return {
                        oIdx: index,
                        group: group,
                        width: width
                    }
                });
            };

            GridProto._restAllTmpl = function () {
                delete this.rowTmpl;
                delete this.compiledRowTmpl;
                delete this.compiledTableTmpl;
                delete this.doneRegisterHelpers;
            };

            GridProto._reSortHeaders = function () {
                var me = this,
                    $headRow = me.element.find('div.grid-head tr:last'),
                    headers = _.initial($headRow.children('th')),
                    tmpledCells = me.hasCheckbox ? _.rest(headers) : headers;
                _.chain(tmpledCells)
                    .map(angular.element)
                    .map(function ($cell) {
                        return {
                            $cell: $cell,
                            idx: _.indexOf(me.columns, getColumn($cell))
                        }
                    })
                    .filter(function (x) {
                        if (x.idx === -1) {
                            x.$cell.hide();
                            return false;
                        }
                        x.$cell.show();
                        return true;
                    })
                    .sortBy(_.property('idx'))
                    .map(_.property('$cell'))
                    .reduce(function ($left, $right) {
                        return $right.insertAfter($left);
                    });

                function getColumn($cell) {
                    return $cell.isolateScope().column;
                }
            };

            GridProto._reSortFooters = function () {
                var me = this,
                    $footerRow = me.element.find('div.grid-foot tr:last'),
                    footers = _.initial($footerRow.children('th')),
                    tmpledCells = me.hasCheckbox ? _.rest(footers) : footers,
                    pairs = _.map(tmpledCells, function (cell) {
                        "use strict";
                        var $cell = angular.element(cell),
                            column = $cell.data(CONSTANTS.DATA_COLUMN_INSTANCE_KEY),
                            idx = _.indexOf(me.columns, column);
                        return {
                            $cell: $cell,
                            idx: idx
                        };
                    });

                _.chain(pairs)
                    .filter(_.matcher({ idx: -1 }))
                    .pluck('$cell')
                    .invoke('hide');

                _.chain(pairs)
                    .filter(_.negate(_.matcher({ idx: -1 })))
                    .sortBy(_.property('idx'))
                    .pluck('$cell')
                    .invoke('show')
                    .reduce(function ($left, $right) {
                        return $right.insertAfter($left);
                    });
            };

            GridProto._startSetting = function ($event) {
                var me = this, menuElement,
                    scope = me.scope;
                if (scope.isGroup) {
                    ColGroupSettings._startSetting(me);
                } else {
                    ColSettings._startSetting(me);
                }
                menuElement = angular.element(document.getElementById('dropdownMenu_' + me.scope.colSettingsKey));
                menuElement.removeClass("open");
                $event.stopPropagation();
            };

            GridProto.doLayout = function (autoSetWidthHeight) {
                var me = this,
                    document = $document[0],
                    documentElement = document.documentElement,
                    body = document.body,
                    bodyWidth = documentElement.clientWidth || body.clientWidth,
                    bodyHeight = documentElement.clientHeight || body.clientHeight,
                    attrs = me.attrs;
                if (!autoSetWidthHeight) {
                    if (attrs.gTop && attrs.gBottom) {
                        me.element.css("height", bodyHeight - attrs.gTop - attrs.gBottom);
                    }
                } else if (attrs.gBottom) {
                    me.element.css("height", bodyHeight - (me.element[0].getBoundingClientRect().top || attrs.gTop) - attrs.gBottom);
                }
                if (attrs.gLeft && attrs.gRight) {
                    me.element.css("width", bodyWidth - attrs.gLeft - attrs.gRight);
                }
            };


            //布局功能
            GridProto.storeLayout = function (layoutContent) {
                var me = this,
                    dataSource = me.scope.dataSource,
                    content;
                if (layoutContent) {
                    var layoutSetting = JSON.parse(layoutContent);
                    me.layoutSetting = layoutSetting.layout;
                    me.sortName = layoutSetting.sortName;
                    me.sortDirection = layoutSetting.sortDirection;
                } else {
                    // 默认布局
                    content = _.map(me.__originalCols, function (col) {
                        var index;
                        index = _.indexOf(me.__originalCols, col);
                        return {
                            oIdx: index
                        }
                    });
                    me.layoutSetting = content;
                    me.sortName = [];
                    me.sortDirection = [];
                }
                if (dataSource) {
                    dataSource.sortName = me.sortName;
                    dataSource.sortDirection = me.sortDirection;
                    if (me.__rendered) {
                        dataSource.doRequestData();
                    }
                } else {
                    $dataSourceManager.getDataSource(me.attrs.sourceName).then(function (dataSource) {
                        dataSource.sortName = me.sortName;
                        dataSource.sortDirection = me.sortDirection;
                        if (me.__rendered) {
                            dataSource.doRequestData();
                        }
                    });
                }
                me.$$modifiedRecords = {};
            };

            //此处是处理布局修改时,重绘grid
            GridProto.reload = function () {
                var me = this, scope = me.scope;
                me._loadAndReSortHeadersIfStored();
                me._restColSettingsBtn();
                me._restAllTmpl();
                me._registerHelpers();
                me._renderCellsStyle();
                if (!(scope.$$phase || scope.$root.$$phase)) {
                    scope.$apply(function () {
                        me._render();
                    });
                } else {
                    me._render();
                }
            };

            GridProto.getLayout = function () {
                var me = this, layout,
                    colSettings = me.layoutSetting;
                layout = _.map(colSettings, function (colSetting) {
                    var index, oIdx, group, width, col;
                    index = _.findIndex(colSettings, colSetting);
                    oIdx = colSetting.oIdx;
                    col = me.columns[index];
                    width = colSetting.width || me.__originalCols[oIdx].width;
                    group = col.group || me.__originalCols[oIdx].group;
                    return {
                        oIdx: oIdx,
                        width: width,
                        group: group
                    }
                });
                return JSON.stringify({
                    layout: layout,
                    sortName: me.sortName,
                    sortDirection: me.sortDirection
                });
            };

            GridProto._registerLayoutService = function () {
                var me = this, menuDiv,
                    tableId = me.scope.colSettingsKey;
                if (!tableId || tableId === "") {
                    return;
                }
                menuDiv = angular.element('<div data-target="dropdownMenu_' + tableId + '" context-menu="" class="ng-isolate-scope" data-hitarea=".grid-head"></div>');
                me.element.wrap($compile(menuDiv)(me.scope));
                gridLayoutService.registerLayout(me.scope, me);

                me.$headerBox.on('mousedown', ':input', function (e) {
                    if (e.button === 2) {
                        this.blur();
                        e.preventDefault();
                    }
                });
            };


            //拖选功能
            GridProto._toggleDragChecked = function ($event, startIndex, endIndex) {
                var me = this, _startIndex, _endIndex;
                _startIndex = startIndex > endIndex ? endIndex : startIndex;
                _endIndex = startIndex > endIndex ? startIndex : endIndex;
                for (var i = 0; i < me.tbody.rows.length; i++) {
                    if (i < _startIndex) {
                        angular.element(me.tbody.rows[i]).removeAttr("active");
                    } else if (i > _endIndex) {
                        angular.element(me.tbody.rows[i]).removeAttr("active");
                    } else {
                        angular.element(me.tbody.rows[i]).attr("active", "true");
                    }
                }
            };

            GridProto._toggleChecked = function (index) {
                var me, scope, source, checkedRows, rowIndex, row, checked, $checkbox;
                me = this;
                $checkbox = angular.element(me.tbody.rows[index]).find('.form-clickbox');
                if ($checkbox.is('[disabled=disabled]')) return;
                scope = me.scope;
                source = scope.source;
                checkedRows = scope.checkedRows;
                rowIndex = $checkbox.closest('tr')[0].rowIndex;
                row = source[rowIndex];
                $checkbox.toggleClass('selected', true);
                checked = _.contains(checkedRows, row);
                if (checked) {
                } else {
                    me.scope.checkedRows.push(source[rowIndex]);
                }
                me.scope.allChecked = me._isAllRowsChecked();
                me.checkRowByContrl = true;
            };

            GridProto._setGridBodyHeight = function () {
                var me = this;
                var $grid = me.element;
                var $gridBody = $grid.find('.grid-body');
                var $gridBodyContent = $gridBody.children();
                var gHeight = $grid[0].style.height;
                var $scrollSpace = me.element.find('.table-scroll-space');
                if (gHeight) {
                    $gridBody.css('height', gHeight);
                }
                $grid.on('scroll', function () {
                    var scrollLeft = $grid.scrollLeft();
                    var bodyWidth = $gridBody.width();
                    var bodyHeight = $gridBody.height();
                    var bodyContentWidth = $gridBodyContent.width();
                    var bodyContentHeight = $gridBodyContent.height();
                    var maxScrollLeft = bodyContentWidth - bodyWidth;
                    var hasScrollBar = bodyContentHeight > bodyHeight;
                    if (!hasScrollBar) {
                        $scrollSpace.hide();
                    } else {
                        $scrollSpace.show();
                    }
                    if (maxScrollLeft < 0) {
                        maxScrollLeft = 0;
                    }
                    if (scrollLeft > maxScrollLeft) {
                        scrollLeft = maxScrollLeft;
                    }
                    scrollLeft = $grid.scrollLeft();
                    $gridBody
                        .css('left', scrollLeft)
                        .scrollLeft(scrollLeft);
                });
                $gridBody.on('scroll', function () {
                    var bodyScrollLeft = $gridBody.scrollLeft();
                    $grid.scrollLeft(bodyScrollLeft);
                    menuElement = angular.element(document.getElementById('dropdownMenu_' + me.scope.colSettingsKey));
                    menuElement.removeClass("open");
                });
                function getScrollBarWidth() {
                    var noScroll, scroll, oDiv = document.createElement("DIV");
                    oDiv.style.cssText = "position:absolute; top:-1000px; width:100px; height:100px; overflow:hidden;";
                    noScroll = document.body.appendChild(oDiv).clientWidth;
                    oDiv.style.overflowY = "scroll";
                    scroll = oDiv.clientWidth;
                    document.body.removeChild(oDiv);
                    return noScroll - scroll;
                }
                $('head').append('<style>th.table-scroll-space{width: ' + getScrollBarWidth() + 'px;}</style>');
            }

            GridProto._clearFilter = function ($event) {
                var me = this;
                delete me.scope.filter
                delete me.scope.pageFilters
                setTimeout(function () {
                    me.scope.filter = {};
                    me.scope.pageFilters = {};
                    me.reload();
                }, 0);
                $event.stopPropagation();
            }

            GridProto._isForbiddenColumn = function(field) {
                var me = this;
                var forbiddenColumns = me.scope.forbiddenColumns || [];
                if (!field) {
                    return false;
                }
                return forbiddenColumns.indexOf(field) >= 0
                    ? true
                    : false;
            }

            return {
                template: '<div class="grid">\n    <div class="grid-head" ng-click="headClick($event)">\n        <table class="table-head">\n            <tbody>\n            <tr>\n                <th class="table-scroll-space"></th>\n            </tr>\n            </thead> </table>\n    </div>\n    <div class="grid-body" ng-click="bodyClick($event)" ng-dblclick="bodyDbClick($event)">\n        <table class="table-body"><tbody></tbody></table>    \n        </div>\n    <div class="col-resize-line">\n    </div>\n</div> ',
                replace: true,
                restrict: 'E',
                transclude: true,
                scope: {
                    sourceName: '@sourceName',
                    validatorName: '@',
                    $columns: '=columns',
                    $selectedRow: '=selectedRow',
                    $checkedRows: '=checkedRows',
                    onLoadSuccess: '&',
                    onRender: '&',
                    onBeforeSortColumn: '&',
                    onSortColumn: '&',
                    onBeforeSelect: '&',
                    onSelect: '&',
                    canToggleSelected: '@',
                    enableEvenClass: '@',
                    onBeforeRowDbclick: '&',
                    onRowDbclick: '&',
                    onBeforeCellDbclick: '&',
                    onCellDbclick: '&',
                    lazy: '@',
                    colSettingsKey: '@',
                    gTop: '@',
                    gBottom: '@',
                    gLeft: '@',
                    gRight: '@',
                    dragCheck: '@',
                    disableCopyEditing: '@',
                    forbiddenColumns: '='
                },
                require: ['gDataGrid', '?gItemClass'],
                compile: function (tElement, tAttributes, transclude) {
                    return function (scope, element, attrs, controllers) {
                        var grid = controllers[0],
                            $head = element.find('.grid-head'),
                            $gridBody = element.find('.grid-body'),
                            $headTableBody = $head.find('tbody'),
                            $table = $gridBody.children('table'),
                            $tbody = $table.children('tbody'),
                            isBindCheckedRows = attrs.hasOwnProperty('checkedRows'),
                            isBindSelectedRow = attrs.hasOwnProperty('selectedRow'),
                            tBodyHeight = attrs.tbodyHeight;
                        scope.isGroup = attrs.group === 'true';
                        scope.draggable = attrs.draggable == "false" ? false : true;
                        if (tBodyHeight) {
                            $gridBody.css("height", tBodyHeight);
                        }
                        //监听筛选器
                        scope.$watch('filter', function (newFilters) {
                            var dataSource = scope.dataSource,
                                params = {
                                    filterName: [],
                                    filterValue: []
                                };
                            if (dataSource) {
                                _.each(newFilters, function (filterVal, filterName) {
                                    if (filterVal === 0 || !!filterVal) {
                                        params.filterName.push(filterName);
                                        params.filterValue.push(filterVal);
                                    }
                                });
                                scope.dataSource.extraParams = _.extendOwn(params, _.omit(dataSource.extraParams, 'filterName', 'filterValue'));
                                if (!dataSource.allowAutoLoad) {
                                    dataSource.doRequestData();
                                    grid.$$modifiedRecords = {};
                                }
                            }
                        }, true);

                        grid.rowClassController = controllers[1];
                        $tbody.data('$gDataGridController', grid);
                        if (scope.validatorName) {
                            grid.formName = 'grid' + grid.hash + 'form';
                            var $form = angular.element('<form>', {
                                name: grid.formName,
                                'g-validator': scope.validatorName,
                                'data-invalid-msg': 'tooltipMessenger',
                                'onsubmit': 'return false'
                            });
                            $table.wrap($form);
                            $form = $gridBody.children('form');
                            $compile($form)(scope);
                            grid.formController = $form.data('$formController');
                            grid.gValidatorController = $form.data('$gValidatorController');
                            $tbody.data('$formController', grid.formController);
                            $tbody.data('$gValidatorController', grid.gValidatorController);
                        }

                        transclude(scope, function (clone) {
                            var headCells = clone.filter('th'),
                                $headSpaceCol = $headTableBody.find('tr th.table-scroll-space'),
                                $foot = clone.filter('.grid-foot');
                            if (headCells.length > 0) {
                                angular.forEach(headCells, function (cell) {
                                    $headSpaceCol.before(cell);
                                });
                            }
                            if ($foot.length > 0) {
                                // init footData
                                grid.footCells = [];
                                element.append($foot);
                                // 绑定 scroll
                                // var bl;
                                // $gridBody.bind('scroll', function (event) {
                                //     var element = (event.target || event.srcElement),
                                //         nbl = element.scrollLeft;
                                //     if (bl !== nbl) {
                                //         $foot[0].scrollLeft = nbl;
                                //         bl = nbl;
                                //     }
                                // });
                            }
                        }, grid);

                        //初始化grid时
                        grid.__originalCols = grid.columns;

                        grid._registerHeaderDraggable();
                        grid._loadAndReSortHeadersIfStored();
                        grid._renderCellsStyle();
                        grid._restColSettingsBtn();
                        grid.doLayout();
                        grid._registerLayoutService();
                        grid._setGridBodyHeight();

                        $dataSourceManager.getDataSource(attrs.sourceName).then(function (result) {
                            scope.dataSource = result;
                        });

                        //设置拖选功能事件
                        scope.dragSelected = [];
                        scope.isMouseDown = false;
                        scope.startIndex = 0;
                        scope.endIndex = 0;
                        if (scope.dragCheck === "true") {
                            scope.dragStyle = { "cursor": "s-resize" };
                            $gridBody.on("mousedown", function ($event) {
                                var target = $event.target;
                                scope.isMouseDown = false;
                                if (target.className === "grid-col-checkbox") {
                                    scope.isMouseDown = true;
                                    var $target = angular.element(target), rowIndex,
                                        $checkbox = $target.find('.form-clickbox');
                                    rowIndex = $checkbox.closest('tr')[0].rowIndex;
                                    scope.startIndex = scope.endIndex = rowIndex;
                                    grid._toggleDragChecked($event, scope.startIndex, scope.endIndex);
                                    return false;
                                }
                            });
                            angular.element(document.body).on("mouseup", function () {
                                var _startIndex = scope.startIndex > scope.endIndex ? scope.endIndex : scope.startIndex,
                                    _endIndex = scope.startIndex > scope.endIndex ? scope.startIndex : scope.endIndex;
                                if (scope.isMouseDown) {
                                    for (var i = _startIndex; i <= _endIndex; i++) {
                                        scope.dragSelected.push(i);
                                        grid._toggleChecked(i);
                                    }
                                }
                                scope.startIndex = scope.endIndex = 0;
                                scope.isMouseDown = false;
                            });
                            $(document).on('click', function() {
                                // grid._toggleDropFilter();
                                $('.drop-filter-list').hide();
                            })
                        }

                        scope.$on(scope.sourceName, function (event, result) {
                            grid.__rendered = true;
                            grid.$$modifiedRecords = {};
                            var body = $document[0].body;
                            if (body.hasAttribute('g-dict') && !!body.getAttribute('g-dict') && scope.$root.$dictReturned !== true) {
                                scope.$on('$dictReturned', function () {
                                    grid._registerHelpers();
                                    doLayer();
                                });
                            } else {
                                grid._registerHelpers();
                                doLayer();
                            }
                            if (scope.dragCheck === "true") {
                                element.find('table.table-body tbody tr').on("mouseover", function ($event) {
                                    var target = $event.target;
                                    if (scope.isMouseDown === true && target.className === "grid-col-checkbox") {
                                        var $target = angular.element($event.target), rowIndex,
                                            $checkbox = $target.find('.form-clickbox');
                                        rowIndex = $checkbox.closest('tr')[0].rowIndex;
                                        scope.endIndex = rowIndex;
                                        grid._toggleDragChecked($event, scope.startIndex, scope.endIndex);
                                    }
                                });
                            }

                            function doLayer() {
                                grid.changeDataSourced = true; //为了标示不是外部改变了数据源
                                scope.source = result['records'];
                                if (result.hasOwnProperty('moreAttrs')) {
                                    scope.moreAttrs = result['moreAttrs'];
                                }

                                grid._render();

                                if (angular.isFunction(scope.onLoadSuccess)) {
                                    scope.onLoadSuccess({
                                        source: scope.source,
                                        grid: grid,
                                        result: result
                                    });
                                }
                            }
                        });

                        scope.headClick = function ($event) {
                            grid._headClick($event);
                        };

                        scope.bodyClick = function ($event) {
                            var target = $event.target || $event.srcElement,
                                $target = angular.element(target),
                                $cell = $target.closest('td'),
                                $row = $cell.closest('tr');
                            if ($target.is('a.fi') && $target.closest('td').is('.grid-col-checkbox')) {
                                grid._toggleCellChecked($event);
                            }
                            if ($row.length === 1) {
                                grid._rowClick($event, $row);
                            }
                        };

                        scope.bodyDbClick = function ($event) {
                            var target = $event.target || $event.srcElement,
                                $target = angular.element(target),
                                $cell = $target.closest('td'),
                                $row = $cell.closest('tr');
                            grid._rowDbClick($event, $row);
                            grid._cellDbClick($event, $cell);
                        };

                        // 实现 grid选中行双向绑定 start
                        scope.$watch('selectedRow', function (newRow, oldRow) {
                            if (newRow !== oldRow) {
                                if (isBindSelectedRow) {
                                    scope.$selectedRow = newRow;
                                }
                            }
                        });

                        if (isBindSelectedRow) {
                            scope.$watch('$selectedRow', function (newRow, oldRow) {
                                var rowIndex, $row;
                                if (newRow !== oldRow && scope.selectedRow !== newRow) {
                                    rowIndex = Arrays.indexOf(scope.source, newRow);
                                    $row = angular.element('tr:nth-child(' + rowIndex + ')', grid.tbody);
                                    grid._rowClick({}, $row);
                                }
                            });
                        }

                        if (isBindCheckedRows) {
                            if (angular.isArray(scope.$checkedRows)) {
                                scope.checkedRows = scope.$checkedRows;
                            } else {
                                scope.checkedRows = scope.$checkedRows = [];
                            }
                            scope.$watchCollection('$checkedRows', function () {
                                if (grid.checkRowByContrl !== true) {
                                    grid._syncCheckRowsByOuter();
                                }
                                grid.checkRowByContrl = false;
                            });
                        } else {
                            scope.checkedRows = [];
                        }

                        // 实现 grid选中行双向绑定 end

                        if (angular.isFunction(scope.onRender)) {
                            scope.onRender({
                                grid: grid,
                                source: scope.source
                            });
                        }

                        // 绑定 scroll
                        // var bl;
                        // $gridBody.bind('scroll', function (event) {
                        //     var element = (event.target || event.srcElement),
                        //         nbl = element.scrollLeft;
                        //     if (bl !== nbl) {
                        //         $head[0].scrollLeft = nbl;
                        //         bl = nbl;
                        //     }
                        // });

                        $($window).on('resize', function () {
                            grid._renderCellsStyle();
                            grid.doLayout(true);
                        });

                        scope.$on('show', function () {
                            grid._renderCellsStyle();
                        });

                        scope.$watchCollection('source', function (newSource, oldSource) {
                            if (grid.changeDataSourced !== true) {
                                grid._syncRows(newSource, oldSource);
                            }
                            grid.changeDataSourced = false;
                        });

                        if (scope.lazy === 'true') {
                            element.children('.grid-body').on('scroll', function () {
                                var st = $(this).scrollTop(),
                                    loadedHeight = (grid._getLazyNextPage() - 2) * grid._getLazyPageSize() * 21;
                                if (st > loadedHeight) {
                                    grid._renderNextLazyPage();
                                }
                            });
                        }
                    };
                },
                controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
                    $scope.grid = new Grid($scope, $element, $attrs);
                    return $scope.grid;
                }]
            };
        })
        .directive('gCheckboxColumn', function () {
            return {
                restrict: 'E',
                template: '<th class="grid-col-checkbox"><g-checkbox ng-model="allChecked"></g-checkbox></th>',
                replace: true,
                scope: false,
                require: ['^gDataGrid', '?gItemDisabled'],
                link: function (scope, element, attrs, controllers) {
                    var gridController = controllers[0],
                        gItemDisabled = controllers[1];
                    gridController.hasCheckbox = true;
                    gridController.checkboxesDisabledController = gItemDisabled;
                    /** @namespace attrs.gItemDisabled */
                    gridController.checkboxesDisabledExpress = attrs.gItemDisabled;
                }
            };
        })
        .directive('gColumn', function (Arrays) {

            function getRealField(rawData) {
                var firstFilterIdx;
                if (rawData) {
                    firstFilterIdx = rawData.indexOf('|');
                    if (firstFilterIdx !== -1) {
                        return $.trim(rawData.substring(0, firstFilterIdx));
                    }
                }
                return rawData;
            }

            function formatDataFilters(rawData) {
                if (!!rawData) {
                    return rawData.replace(/\s*\|\s*/g, ' | ');
                }
                return rawData;
            }

            function getCellAlignClass(scope) {
                return 'align-' + (scope.cellAlign || scope.align || 'left');
            }

            return {
                template: '<th ng-hide="isForbidden || hidden">\n    <div class="grid-head-sort" ng-if="sortable !== undefined">\n        <button class="btn">\n            <span ng-class="{\'selected\': grid._isSort(sortable, \'asc\')}" class="caret caret-up"></span>\n        </button>\n        <button class="btn">\n            <span ng-class="{\'selected\': grid._isSort(sortable, \'desc\')}" class="caret caret-down"></span>\n        </button>\n    </div>\n    <sub ng-if="grid._sortIndex(sortable)!==\'\'">{{grid._sortIndex(sortable)}}</sub>\n    <div class="drop-filter" ng-if="dropFilter !== undefined"><div class="drop-filter-toggle"></div><div class="drop-filter-list" style="display: none;"><ul g-checkbox-group ng-model="grid.scope.pageFilters[field]"><li ng-repeat="record in grid.scope.dataSource.records | removeRepeat:field"><g-checkbox display="{{record}}" value="{{record}}" inverse-check></g-checkbox></li></ul></div></div></th>',
                replace: true,
                restrict: 'E',
                transclude: true,
                scope: {
                    $sortable: '@sortable',
                    data: '@',
                    width: '@',
                    editable: '@',
                    filter: '@',
                    tmpl: '@',
                    disableTitle: '@',
                    align: '@',
                    cellAlign: '@',
                    dropFilter: '@',
                    required: '@'
                },
                require: ['^gDataGrid', '?gItemClass'],
                compile: function (tElement, tAttrs, transclude) {
                    return function (scope, element, attrs, controllers) {
                        var gridController = controllers[0],
                            gItemClass = controllers[1];
                        transclude(scope, function (clone) {
                            angular.forEach(clone, function (node) {
                                if (!/G-COLUMN-TMPL|G-COLUMN-EDITOR|G-COLUMN-FILTER/i.test(node.tagName)) {
                                    element.append(node);
                                }
                            });
                        }, gridController);
                        var columnInstance = {
                            field: getRealField(scope.data),
                            data: formatDataFilters(scope.data),
                            text: $.trim(element.text()),
                            widthDef: scope.width,
                            editable: scope.editable,
                            tmpl: scope.tmpl,
                            filter: scope.filter,
                            filterTmpl: scope.filterTmpl,
                            element: element,
                            cellAlignClass: getCellAlignClass(scope),
                            gItemClass: gItemClass,
                            disableTitle: scope.disableTitle === 'true'
                        },
                            newLength = gridController._addColumn(columnInstance),
                            colIndex = newLength - 1,
                            cellIndex = element[0].cellIndex,
                            onBeforeCellDbclickDef = attrs.onBeforeCellDbclick,
                            onCellDbclickDef = attrs.onCellDbclick,
                            required = false,
                            hidden = false,
                            sortable;
                        // 如果需要排序， data中有空格视为包含过滤器， 截取空格前字符串作为排序字段
                        if (attrs.hasOwnProperty('sortable')) {
                            sortable = columnInstance.field;
                        }

                        if (attrs.hasOwnProperty('required')) {
                            required = true;
                        }

                        if (attrs.hasOwnProperty('hidden')) {
                            hidden = true;
                        }

                        if (!!scope.editorTmpl) {
                            columnInstance.editorTmpl = scope.editorTmpl;
                        }
                        if (columnInstance.field && (columnInstance.editable || columnInstance.editorTmpl)) {
                            columnInstance.canEdit = true;
                        }
                        // 列双击事件
                        if (onBeforeCellDbclickDef) {
                            columnInstance.onBeforeCellDbclick = gridController._getScopeEvent(onBeforeCellDbclickDef);
                        }
                        if (onCellDbclickDef) {
                            columnInstance.onCellDbclick = gridController._getScopeEvent(onCellDbclickDef);
                        }

                        columnInstance.colWidthClassName = gridController._getColWidthClassName(cellIndex);
                        columnInstance.index = colIndex;
                        columnInstance.sortable = scope.sortable = sortable;
                        columnInstance.required = scope.required = required;
                        columnInstance.hidden = scope.hidden = hidden;

                        scope.field = getRealField(scope.data);
                        scope.column = columnInstance;
                        scope.grid = gridController;
                        scope.isForbidden = gridController._isForbiddenColumn(scope.field);
                        attrs.$addClass(columnInstance.colWidthClassName);

                        //拖拽功能 *****begin*******
                        if (gridController.scope.draggable) {
                            element.attr("draggable", "true");
                            element[0].addEventListener("dragover", function (event) {
                                event.preventDefault();
                            });
                            element[0].addEventListener("drop", function (event) {
                                event.preventDefault();
                                var begin_column = gridController.element.data("_begin_column");
                                if (Arrays.indexOf(gridController.columns, begin_column) <= Arrays.indexOf(gridController.columns, scope.column)) {
                                    gridController.columns = Arrays.moveAfter(gridController.columns, begin_column, scope.column);
                                } else {
                                    gridController.columns = Arrays.moveBefore(gridController.columns, begin_column, scope.column);
                                }

                                gridController._enterCols(gridController.columns);
                            });
                            element[0].ondragstart = function (event) {
                                gridController.element.data("_begin_column", columnInstance);
                            };
                        }
                        //拖拽功能 *****end*******
                    }
                }
            };
        })
        .directive('gColumnFilter', function () {
            return {
                restrict: 'E',
                scope: false,
                compile: function (tElement) {
                    var filterTmpl = $.trim(tElement.html());
                    if (navigator.appName == "Microsoft Internet Explorer" && navigator.appVersion.split(";")[1].replace(/[ ]/g, "") == "MSIE8.0") {
                        filterTmpl = filterTmpl.replace('<:', '<').replace('<:/', '</');
                    }
                    return function (scope, element) {
                        scope.filterTmpl = filterTmpl;
                    }
                }
            };
        })
        .directive('cellTemplated', function ($compile) {
            return {
                restrict: 'A',
                scope: true,
                require: '^gDataGrid',
                link: function (scope, element, attrs, grid) {
                    //noinspection JSUnresolvedVariable
                    var columnIndex = grid._getColumnIndex(element),
                        tr = element.parent()[0],
                        rowIndex = tr.rowIndex,
                        row = grid.scope.source[rowIndex],
                        column = grid.columns[columnIndex],
                        $displayPlace = element.children('[data-role=display]'),
                        tmplScope = grid._getCellTmplScope({
                            rowIndex: rowIndex,
                            column: column,
                            row: row,
                            columnIndex: columnIndex
                        });
                    $displayPlace.html(column.tmpl);
                    $compile($displayPlace)(tmplScope);
                    tmplScope.$on(CONSTANTS.REFRESH_ROWS, function (event, range) {
                        if (_.contains(range, tmplScope.rowIndex)) {
                            tmplScope.rowIndex = tr.rowIndex;
                        }
                    });
                }
            };
        })
        .directive('gFooter', function () {
            return {
                restrict: 'E',
                template: '<div class="grid-foot"> <table class="table-foot"> <tbody> <tr> <th class="table-scroll-space"></th> </tr> </tbody> </table> </div>',
                replace: true,
                transclude: true,
                scope: false,
                require: '^gDataGrid',
                compile: function (tElement, tAttrs, transclude) {
                    return function (scope, element, attrs, gridController) {
                        var $footSpaceCell = element.find('.table-scroll-space');
                        transclude(scope, function (clone) {
                            var footCells = clone.filter('th');
                            $footSpaceCell.before(footCells);
                        }, gridController);
                    }
                }
            };
        })
        .directive('gFooterCell', function () {
            return {
                restrict: 'E',
                template: '<th ng-hide="isForbidden"><div ng-transclude/> </th>',
                replace: true,
                transclude: true,
                scope: false,
                require: '^gDataGrid',
                link: function (scope, element, attrs, gridController) {
                    var span = Number(element.attr('colspan')),
                        spanColWidthClassName = gridController._registerFootCellWidthClass(span),
                        colIdx = gridController._getColumnIndex(element);
                    var column = gridController.columns[colIdx];
                    var isForbidden = gridController._isForbiddenColumn(column ? column.field : null);
                    var hidden = false;
                    if (column) {
                        hidden = column.hidden;
                    }
                    element.data(CONSTANTS.DATA_COLUMN_INSTANCE_KEY, column);
                    attrs.$addClass(spanColWidthClassName);
                    if (isForbidden || hidden) {
                        attrs.$addClass('hidden');
                    }
                }
            };
        })
        // just for stored column's template
        .directive('gColumnTmpl', function () {
            return {
                restrict: 'E',
                scope: false,
                compile: function (tElement) {
                    var columnTmpl = $.trim(tElement.html());
                    if (navigator.appName == "Microsoft Internet Explorer" && navigator.appVersion.split(";")[1].replace(/[ ]/g, "") == "MSIE8.0") {
                        columnTmpl = columnTmpl.replace('<:', '<').replace('<:/', '</');
                    }
                    return function (scope) {
                        scope.tmpl = columnTmpl;
                    }
                }
            };
        })
        // NOTE 一个表格中只能定义一个删除按钮
        .directive('gActionRemove', function (Arrays) {
            return {
                restrict: 'E',
                replace: 'true',
                template: '<a href="javascript:;" ng-click="removeRecord($event)" class="btn" title="删除"><i class="fi fi-del"></i></a>',
                scope: false,
                link: function (scope, element, attrs) {
                    var grid = scope.grid,
                        onBeforeActionAttrVal = attrs['onBeforeAction'],
                        onActionAttrVal = attrs['onAction'],
                        onBeforeAction = angular.noop,
                        onAction = angular.noop;
                    if (grid) {
                        if (!!onBeforeActionAttrVal) {
                            onBeforeAction = grid._getScopeEvent(onBeforeActionAttrVal);
                        }
                        if (!!onActionAttrVal) {
                            onAction = grid._getScopeEvent(onActionAttrVal);
                        }
                    }

                    scope.removeRecord = function ($event) {
                        var element = $event.target || $event.srcElement,
                            $row = angular.element(element).closest('tr'),
                            rowIndex = $row[0].rowIndex,
                            source = grid.scope.source,
                            row = source[rowIndex];
                        //noinspection JSUnresolvedVariable
                        var eventParams = {
                            $event: $event,
                            record: row
                        };
                        if (onBeforeAction(eventParams) !== false) {
                            Arrays.remove(grid.scope.checkedRows, source[rowIndex]);
                            source.splice(rowIndex, 1);
                            onAction(eventParams);
                        }
                    }
                }
            };
        })
        .directive('gColumnEditor', function () {
            return {
                restrict: 'E',
                scope: false,
                compile: function (tElement) {
                    var editorTmpl = $.trim(tElement.html());
                    if (navigator.appName == "Microsoft Internet Explorer" && navigator.appVersion.split(";")[1].replace(/[ ]/g, "") == "MSIE8.0") {
                        editorTmpl = editorTmpl.replace('<:', '<').replace('<:/', '</');
                    }
                    return function (scope) {
                        scope.editorTmpl = editorTmpl;
                    }
                }
            };
        })
        .directive('renderCheckboxDisabled', function () {
            return {
                require: 'outerScope',
                scope: true,
                link: function (scope, element, attrs, outerScopeController) {
                    var gItemDisabledController = scope.grid.checkboxesDisabledController,
                        locals, rowIndex, tr;
                    if (angular.isFunction(gItemDisabledController)) {
                        tr = element.closest('tr')[0];
                        rowIndex = tr.rowIndex;
                        locals = {
                            rowIndex: rowIndex,
                            row: scope.source[rowIndex]
                        };
                        angular.extend(scope, locals);
                        gItemDisabledController({
                            locals: locals,
                            scope: scope,
                            element: element,
                            outerScopeController: outerScopeController
                        });

                        scope.$on(CONSTANTS.REFRESH_ROWS, function (event, range) {
                            rowIndex = tr.rowIndex;
                            if (_.contains(range, rowIndex)) {
                                scope.rowIndex = rowIndex;
                            }
                        });
                    }
                }
            };
        })
        .directive('renderRowClass', function () {
            return {
                require: 'outerScope',
                scope: true,
                link: function (scope, element, attrs, outerScopeController) {
                    var rowClassController = scope.grid.rowClassController,
                        locals, rowIndex, tr;
                    if (angular.isFunction(rowClassController)) {
                        tr = element.closest('tr')[0];
                        rowIndex = tr.rowIndex;
                        locals = {
                            rowIndex: rowIndex,
                            row: scope.source[rowIndex]
                        };
                        angular.extend(scope, locals);
                        rowClassController({
                            locals: locals,
                            scope: scope,
                            element: element,
                            attrs: attrs,
                            outerScopeController: outerScopeController
                        });

                        scope.$on(CONSTANTS.REFRESH_ROWS, function (event, range) {
                            rowIndex = tr.rowIndex;
                            if (_.contains(range, rowIndex)) {
                                scope.rowIndex = rowIndex;
                            }
                        });
                    }
                }
            };
        })
        .directive('renderCellClass', function() {
            return {
                require: 'outerScope',
                scope: true,
                link: function (scope, element, attrs, outerScopeController) {
                    var cell = element.closest('td')[0],
                        grid = scope.grid,
                        columnIndex = grid._getColumnIndex(cell),
                        column = scope.grid.columns[columnIndex],
                        gItemClass = column.gItemClass,
                        locals, rowIndex, tr;
                    if (column && angular.isFunction(gItemClass)) {
                        tr = cell.parentNode;
                        rowIndex = tr.rowIndex;
                        locals = {
                            columnIndex: columnIndex,
                            column: column,
                            rowIndex: rowIndex,
                            row: scope.source[rowIndex]
                        };
                        angular.extend(scope, locals);
                        gItemClass({
                            locals: locals,
                            scope: scope,
                            attrs: attrs,
                            element: element,
                            outerScopeController: outerScopeController
                        });

                        scope.$on(CONSTANTS.REFRESH_ROWS, function (event, range) {
                            rowIndex = tr.rowIndex;
                            if (_.contains(range, rowIndex)) {
                                scope.rowIndex = rowIndex;
                            }
                        });
                    }
                }
            };
        })
        .filter('removeRepeat', function() {
            return function(records, field) {
                var out = [];
                _.forEach(records, function(record) {
                    var val = record[field];
                    if (out.indexOf(val) < 0) {
                        out.push(val);
                    }
                });
                return out;
            }
        });
});